/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ru.playsoftware.j2meloader.util;

import org.xmlpull.v1.XmlSerializer;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;

/**
 * This is a quick and dirty implementation of XmlSerializer that isn't horribly
 * painfully slow like the normal one.  It only does what is needed for the
 * specific XML files being written with it.
 */
public class FastXmlSerializer implements XmlSerializer {
	private static final String ESCAPE_TABLE[] = new String[]{
			"&#0;", "&#1;", "&#2;", "&#3;", "&#4;", "&#5;", "&#6;", "&#7;",  // 0-7
			"&#8;", "&#9;", "&#10;", "&#11;", "&#12;", "&#13;", "&#14;", "&#15;", // 8-15
			"&#16;", "&#17;", "&#18;", "&#19;", "&#20;", "&#21;", "&#22;", "&#23;", // 16-23
			"&#24;", "&#25;", "&#26;", "&#27;", "&#28;", "&#29;", "&#30;", "&#31;", // 24-31
			null, null, "&quot;", null, null, null, "&amp;", null,   // 32-39
			null, null, null, null, null, null, null, null,   // 40-47
			null, null, null, null, null, null, null, null,   // 48-55
			null, null, null, null, "&lt;", null, "&gt;", null,   // 56-63
	};

	private static final int DEFAULT_BUFFER_LEN = 32 * 1024;

	private static String sSpace = "                                                              ";

	private final int mBufferLen;
	private final char[] mText;
	private int mPos;

	private Writer mWriter;

	private OutputStream mOutputStream;
	private CharsetEncoder mCharset;
	private ByteBuffer mBytes;

	private boolean mIndent = false;
	private boolean mInTag;

	private int mNesting = 0;
	private boolean mLineStart = true;

	public FastXmlSerializer() {
		this(DEFAULT_BUFFER_LEN);
	}

	/**
	 * Allocate a FastXmlSerializer with the given internal output buffer size.  If the
	 * size is zero or negative, then the default buffer size will be used.
	 *
	 * @param bufferSize Size in bytes of the in-memory output buffer that the writer will use.
	 */
	private FastXmlSerializer(int bufferSize) {
		mBufferLen = (bufferSize > 0) ? bufferSize : DEFAULT_BUFFER_LEN;
		mText = new char[mBufferLen];
		mBytes = ByteBuffer.allocate(mBufferLen);
	}

	private void append(char c) throws IOException {
		int pos = mPos;
		if (pos >= (mBufferLen - 1)) {
			flush();
			pos = mPos;
		}
		mText[pos] = c;
		mPos = pos + 1;
	}

	private void append(String str, int i, final int length) throws IOException {
		if (length > mBufferLen) {
			final int end = i + length;
			while (i < end) {
				int next = i + mBufferLen;
				append(str, i, next < end ? mBufferLen : (end - i));
				i = next;
			}
			return;
		}
		int pos = mPos;
		if ((pos + length) > mBufferLen) {
			flush();
			pos = mPos;
		}
		str.getChars(i, i + length, mText, pos);
		mPos = pos + length;
	}

	private void append(char[] buf, int i, final int length) throws IOException {
		if (length > mBufferLen) {
			final int end = i + length;
			while (i < end) {
				int next = i + mBufferLen;
				append(buf, i, next < end ? mBufferLen : (end - i));
				i = next;
			}
			return;
		}
		int pos = mPos;
		if ((pos + length) > mBufferLen) {
			flush();
			pos = mPos;
		}
		System.arraycopy(buf, i, mText, pos, length);
		mPos = pos + length;
	}

	private void append(String str) throws IOException {
		append(str, 0, str.length());
	}

	private void appendIndent(int indent) throws IOException {
		indent *= 4;
		if (indent > sSpace.length()) {
			indent = sSpace.length();
		}
		append(sSpace, 0, indent);
	}

	private void escapeAndAppendString(final String string) throws IOException {
		final int N = string.length();
		final char NE = (char) ESCAPE_TABLE.length;
		final String[] escapes = ESCAPE_TABLE;
		int lastPos = 0;
		int pos;
		for (pos = 0; pos < N; pos++) {
			char c = string.charAt(pos);
			if (c >= NE) continue;
			String escape = escapes[c];
			if (escape == null) continue;
			if (lastPos < pos) append(string, lastPos, pos - lastPos);
			lastPos = pos + 1;
			append(escape);
		}
		if (lastPos < pos) append(string, lastPos, pos - lastPos);
	}

	private void escapeAndAppendString(char[] buf, int start, int len) throws IOException {
		final char NE = (char) ESCAPE_TABLE.length;
		final String[] escapes = ESCAPE_TABLE;
		int end = start + len;
		int lastPos = start;
		int pos;
		for (pos = start; pos < end; pos++) {
			char c = buf[pos];
			if (c >= NE) continue;
			String escape = escapes[c];
			if (escape == null) continue;
			if (lastPos < pos) append(buf, lastPos, pos - lastPos);
			lastPos = pos + 1;
			append(escape);
		}
		if (lastPos < pos) append(buf, lastPos, pos - lastPos);
	}

	@Override
	public XmlSerializer attribute(String namespace, String name, String value) throws IOException,
			IllegalArgumentException, IllegalStateException {
		append(' ');
		if (namespace != null) {
			append(namespace);
			append(':');
		}
		append(name);
		append("=\"");

		escapeAndAppendString(value);
		append('"');
		mLineStart = false;
		return this;
	}

	@Override
	public void cdsect(String text) throws IOException, IllegalArgumentException,
			IllegalStateException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void comment(String text) throws IOException, IllegalArgumentException,
			IllegalStateException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void docdecl(String text) throws IOException, IllegalArgumentException,
			IllegalStateException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void endDocument() throws IOException, IllegalArgumentException, IllegalStateException {
		flush();
	}

	@Override
	public XmlSerializer endTag(String namespace, String name) throws IOException,
			IllegalArgumentException, IllegalStateException {
		mNesting--;
		if (mInTag) {
			append(" />\n");
		} else {
			if (mIndent && mLineStart) {
				appendIndent(mNesting);
			}
			append("</");
			if (namespace != null) {
				append(namespace);
				append(':');
			}
			append(name);
			append(">\n");
		}
		mLineStart = true;
		mInTag = false;
		return this;
	}

	@Override
	public void entityRef(String text) throws IOException, IllegalArgumentException,
			IllegalStateException {
		throw new UnsupportedOperationException();
	}

	private void flushBytes() throws IOException {
		int position;
		if ((position = mBytes.position()) > 0) {
			mBytes.flip();
			mOutputStream.write(mBytes.array(), 0, position);
			mBytes.clear();
		}
	}

	@Override
	public void flush() throws IOException {
		//Log.i("PackageManager", "flush mPos=" + mPos);
		if (mPos > 0) {
			if (mOutputStream != null) {
				CharBuffer charBuffer = CharBuffer.wrap(mText, 0, mPos);
				CoderResult result = mCharset.encode(charBuffer, mBytes, true);
				while (true) {
					if (result.isError()) {
						throw new IOException(result.toString());
					} else if (result.isOverflow()) {
						flushBytes();
						result = mCharset.encode(charBuffer, mBytes, true);
						continue;
					}
					break;
				}
				flushBytes();
				mOutputStream.flush();
			} else {
				mWriter.write(mText, 0, mPos);
				mWriter.flush();
			}
			mPos = 0;
		}
	}

	@Override
	public int getDepth() {
		throw new UnsupportedOperationException();
	}

	@Override
	public boolean getFeature(String name) {
		throw new UnsupportedOperationException();
	}

	@Override
	public String getName() {
		throw new UnsupportedOperationException();
	}

	@Override
	public String getNamespace() {
		throw new UnsupportedOperationException();
	}

	@Override
	public String getPrefix(String namespace, boolean generatePrefix)
			throws IllegalArgumentException {
		throw new UnsupportedOperationException();
	}

	@Override
	public Object getProperty(String name) {
		throw new UnsupportedOperationException();
	}

	@Override
	public void ignorableWhitespace(String text) throws IOException, IllegalArgumentException,
			IllegalStateException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void processingInstruction(String text) throws IOException, IllegalArgumentException,
			IllegalStateException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setFeature(String name, boolean state) throws IllegalArgumentException,
			IllegalStateException {
		if (name.equals("http://xmlpull.org/v1/doc/features.html#indent-output")) {
			mIndent = true;
			return;
		}
		throw new UnsupportedOperationException();
	}

	@Override
	public void setOutput(OutputStream os, String encoding) throws IOException,
			IllegalArgumentException, IllegalStateException {
		if (os == null)
			throw new IllegalArgumentException();
		if (true) {
			try {
				mCharset = Charset.forName(encoding).newEncoder()
						.onMalformedInput(CodingErrorAction.REPLACE)
						.onUnmappableCharacter(CodingErrorAction.REPLACE);
			} catch (IllegalCharsetNameException e) {
				throw (UnsupportedEncodingException) (new UnsupportedEncodingException(
						encoding).initCause(e));
			} catch (UnsupportedCharsetException e) {
				throw (UnsupportedEncodingException) (new UnsupportedEncodingException(
						encoding).initCause(e));
			}
			mOutputStream = os;
		} else {
			setOutput(
					encoding == null
							? new OutputStreamWriter(os)
							: new OutputStreamWriter(os, encoding));
		}
	}

	@Override
	public void setOutput(Writer writer) throws IOException, IllegalArgumentException,
			IllegalStateException {
		mWriter = writer;
	}

	@Override
	public void setPrefix(String prefix, String namespace) throws IOException,
			IllegalArgumentException, IllegalStateException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void setProperty(String name, Object value) throws IllegalArgumentException,
			IllegalStateException {
		throw new UnsupportedOperationException();
	}

	@Override
	public void startDocument(String encoding, Boolean standalone) throws IOException,
			IllegalArgumentException, IllegalStateException {
		append("<?xml version='1.0' encoding='utf-8' standalone='"
				+ (standalone ? "yes" : "no") + "' ?>\n");
		mLineStart = true;
	}

	@Override
	public XmlSerializer startTag(String namespace, String name) throws IOException,
			IllegalArgumentException, IllegalStateException {
		if (mInTag) {
			append(">\n");
		}
		if (mIndent) {
			appendIndent(mNesting);
		}
		mNesting++;
		append('<');
		if (namespace != null) {
			append(namespace);
			append(':');
		}
		append(name);
		mInTag = true;
		mLineStart = false;
		return this;
	}

	@Override
	public XmlSerializer text(char[] buf, int start, int len) throws IOException,
			IllegalArgumentException, IllegalStateException {
		if (mInTag) {
			append(">");
			mInTag = false;
		}
		escapeAndAppendString(buf, start, len);
		if (mIndent) {
			mLineStart = buf[start + len - 1] == '\n';
		}
		return this;
	}

	@Override
	public XmlSerializer text(String text) throws IOException, IllegalArgumentException,
			IllegalStateException {
		if (mInTag) {
			append(">");
			mInTag = false;
		}
		escapeAndAppendString(text);
		if (mIndent) {
			mLineStart = text.length() > 0 && (text.charAt(text.length() - 1) == '\n');
		}
		return this;
	}

}